﻿#region using statements

using System;
using System.Collections.Generic;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

#endregion

namespace NGen.Systems.Graphics
{
	public class AnimatedSprite
	{
		#region local structs

		/// <summary>
		/// 	struct used by the dictionary to stores animation start/end frames
		/// </summary>
		private struct AnimationContainer
		{
			internal Point m_endFrame;

			internal Point m_startFrame;

			internal AnimationContainer SetData(Point p_startFrame, Point p_endFrame) {
				m_startFrame = p_startFrame;
				m_endFrame = p_endFrame;
				return this;
			}
		};

		#endregion

		#region local delegates

		///<summary>
		///	delegate defining animation events
		///</summary>
		///<param name = "p_animationName">the animation name</param>
		public delegate void AnimationEventHandler(string p_animationName);

		#endregion

		#region local fields

		/// <summary>
		/// 	animation name to animate the entire spritesheet
		/// </summary>
		public const string c_DEFAULT_ANIMATION_NAME = "full";

		///<summary>
		///	dictionary which maps the animation name to the start/end frames
		///</summary>
		private readonly Dictionary<string, AnimationContainer> m_dAnimation = new Dictionary<string, AnimationContainer>();

		///<summary>
		///	x,y coordinates of the current frame
		///</summary>
		private Point m_animationCurrentFrame;

		///<summary>
		///	stores the dimensions of the current frame
		///</summary>
		private Rectangle m_animationFrameDimensions;

		///<summary>
		///	delay between animation changes
		///</summary>
		private float m_animationTimer;

		///<summary>
		///	event which stores animation changed listeners
		///</summary>
		private event AnimationEventHandler MAnimationChangedEvent;

		///<summary>
		///	event which stores animation ended listeners
		///</summary>
		private event AnimationEventHandler MAnimationEndedEvent;

		#endregion

		#region local methods

		///<summary>
		///	fire an animation changed event
		///</summary>
		///<param name = "p_animationName">the animation name</param>
		protected void ThrowAnimationChangedEvent(string p_animationName) {
			if(MAnimationChangedEvent != null) {
				MAnimationChangedEvent(p_animationName);
			}
		}

		///<summary>
		///	fire an animation ended event
		///</summary>
		///<param name = "p_animationName">the animation name</param>
		protected void ThrowAnimationEndedEvent(string p_animationName) {
			if(MAnimationEndedEvent != null) {
				MAnimationEndedEvent(p_animationName);
			}
		}

		#endregion

		#region constructors

		/// <summary>
		/// 	construct a new animated sprite
		/// </summary>
		/// <param name = "p_spriteSheet">the spritesheet image resource name</param>
		/// <param name = "p_frameSize">the size (in pixels) of a single frame</param>
		/// <param name = "p_sheetSize">the dimensions of the spritesheet (in frames)</param>
		/// <param name = "p_frameIncrementDelay">the amount of time between animation changes</param>
		public AnimatedSprite(Texture2D p_spriteSheet, Point p_frameSize, Point p_sheetSize, float p_frameIncrementDelay) {
			AnimationSpriteSheet = p_spriteSheet;
			AnimationSheetSize = p_sheetSize;
			AnimationCurrentFrame = Point.Zero;

			AnimationFrameSize = p_frameSize;
			Origin = new Vector2(p_frameSize.X /2f, p_frameSize.Y);
			Enabled = true;

			AddAnimation(c_DEFAULT_ANIMATION_NAME, new Point(1,1), p_sheetSize);
			ChangeAnimation(c_DEFAULT_ANIMATION_NAME);
			FrameIncrementDelay = p_frameIncrementDelay;
			m_animationTimer = p_frameIncrementDelay;
			m_animationFrameDimensions = new Rectangle(0, 0, p_frameSize.X, p_frameSize.Y);
		}

		#endregion

		#region public properties

		/// <summary>
		/// 	get the current spritesheet animation name
		/// </summary>
		public string CurrentAnimation { get; protected set; }

		/// <summary>
		/// 	get the spritesheet contaning tha animated frames
		/// </summary>
		public Texture2D AnimationSpriteSheet { get; protected set; }

		/// <summary>
		/// 	gets the current animation frame
		/// </summary>
		public Point AnimationCurrentFrame {
			get { return m_animationCurrentFrame; }
			protected set { m_animationCurrentFrame = value; }
		}

		/// <summary>
		/// 	get the current animations' start frame
		/// </summary>
		public Point AnimationStartFrame { get; protected set; }

		/// <summary>
		/// 	get the current animations' end frame
		/// </summary>
		public Point AnimationEndFrame { get; protected set; }

		/// <summary>
		/// 	get the current animation sprite sheet dimensions
		/// </summary>
		public Point AnimationSheetSize { get; protected set; }

		/// <summary>
		/// 	get the current animation sprite sheet frame size
		/// </summary>
		public Point AnimationFrameSize { get; protected set; }
		
		/// <summary>
		/// used to offset the image from the topleft corner
		/// </summary>
		public Vector2 Origin { get; protected set; }

		/// <summary>
		/// 	get the amount of time before changing to the next frame in the animation
		/// </summary>
		public float FrameIncrementDelay { get; set; }

		/// <summary>
		/// 	get or set animation toggle
		/// </summary>
		public bool Enabled { get; set; }

		#endregion

		#region public methods

		/// <summary>
		/// 	add or remove listening to when the animation is changed
		/// </summary>
		public event AnimationEventHandler AnimationChangedEvent {
			add { MAnimationChangedEvent += value; }
			remove { MAnimationChangedEvent -= value; }
		}

		/// <summary>
		/// 	add or remove listening to when the animation reaches its last frame
		/// </summary>
		public event AnimationEventHandler AnimationEndedEvent {
			add { MAnimationEndedEvent += value; }
			remove { MAnimationEndedEvent -= value; }
		}

		/// <summary>
		/// 	update to the next image in the spritesheet automatically after the delay has passed
		/// </summary>
		/// <param name = "p_gameTime">snapshot of timing values</param>
		public void Update(GameTime p_gameTime) {
			if(!Enabled) {
				return;
			}
			m_animationTimer -= (float) p_gameTime.ElapsedGameTime.TotalSeconds;

			//if the timer has expired, change to next frame
			while(m_animationTimer < 0f) {
				//if end of animation reached
				if(AnimationCurrentFrame.X >= AnimationEndFrame.X &&
				   AnimationCurrentFrame.Y >= AnimationEndFrame.Y) {
					ThrowAnimationEndedEvent(CurrentAnimation);
					m_animationCurrentFrame.X = AnimationStartFrame.X;
					m_animationCurrentFrame.Y = AnimationStartFrame.Y;
				} else { //go to next frame
					m_animationCurrentFrame.X++;
				}

				//if end of row, go to next row
				if(AnimationCurrentFrame.X >=
				   AnimationSheetSize.X) {
					m_animationCurrentFrame.X = 0;
					m_animationCurrentFrame.Y++;
				}

				//if end of sheet reached
				if(AnimationCurrentFrame.Y >=
				   AnimationSheetSize.Y) {
					m_animationCurrentFrame = Point.Zero;
				}

				m_animationTimer += FrameIncrementDelay;
			}
		}

		/// <summary>
		/// 	get the the current animation frame
		/// </summary>
		/// <returns>returns the rectagle with position and dimensions of the current animation frame</returns>
		public Rectangle GetFrame() {
			m_animationFrameDimensions.X = AnimationCurrentFrame.X * AnimationFrameSize.X;
			m_animationFrameDimensions.Y = AnimationCurrentFrame.Y * AnimationFrameSize.Y;
			return m_animationFrameDimensions;
		}

		/// <summary>
		/// 	specify a new animation. there is no bounds checking. animation name must be unique.
		/// </summary>
		/// <param name = "p_animationName">name of the animation to use</param>
		/// <param name = "p_startFrame">starting frame (x,y)</param>
		/// <param name = "p_endFrame">ending frame(x,y)</param>
		public void AddAnimation(string p_animationName, Point p_startFrame, Point p_endFrame) {
			if(m_dAnimation.ContainsKey(p_animationName)) {
				throw new Exception("animation name is already registered: " + p_animationName);
			}

			p_startFrame.X -= 1;
			p_startFrame.Y -= 1;
			p_endFrame.X -= 1;
			p_endFrame.Y -= 1;

			m_dAnimation.Add(p_animationName, new AnimationContainer().SetData(p_startFrame, p_endFrame));
		}

		/// <summary>
		/// 	changes the current animation to the animation specified
		/// </summary>
		/// <param name = "p_animationName">the name of the animation desired</param>
		public void ChangeAnimation(string p_animationName) {
			if(!m_dAnimation.ContainsKey(p_animationName)) {
				throw new Exception("animation name is not registered: " + p_animationName);
			}

			CurrentAnimation = p_animationName;

			var animationProps = m_dAnimation[p_animationName];

			m_animationCurrentFrame.X = animationProps.m_startFrame.X;
			m_animationCurrentFrame.Y = animationProps.m_startFrame.Y;

			AnimationStartFrame = animationProps.m_startFrame;
			AnimationEndFrame = animationProps.m_endFrame;

			ThrowAnimationChangedEvent(p_animationName);
		}

		/// <summary>
		///		resets the animation to its first frame
		/// </summary>
		public void ResetAnimation() {
			m_animationCurrentFrame.X = AnimationStartFrame.X;
			m_animationCurrentFrame.Y = AnimationStartFrame.Y;
		}

		#endregion
	}
}